package org.fixies.spreadsheet.excel.poi;


import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.fixies.spreadsheet.excel.ExcelSheetContext;
import org.fixies.utils.CollectionUtils;
import org.fixies.utils.UnexpectedProblemException;
import org.fixies.utils.Validator;

import java.util.ArrayList;
import java.util.List;


public class HSSFSheetContext extends ExcelSheetContext {

    /**
     * HSSFSheet
     */
    private final HSSFSheet hssfSheet_;

    /**
     * 現在処理中の行数
     */
    private int currentRowNumber_;

    /**
     * カーソルが移動したか
     */
    private boolean moved_ = true;

    /**
     * 現在の行
     */
    private String[] currentRow_;

    /**
     * シート名
     */
    private String sheetName_;

    /**
     * コンバータ
     */
    private HSSFCellValueConverter converter_ = new DefaultHSSFCellValueConverter();

    /**
     * コンストラクタ
     *
     * @param hssfSheet HSSFSheet
     * @param sheetName シート名
     */
    public HSSFSheetContext(HSSFSheet hssfSheet, String sheetName) {
        Validator.notNull(hssfSheet);
        Validator.notNullOrEmpty(sheetName);
        hssfSheet_ = hssfSheet;
        sheetName_ = sheetName;
        currentRowNumber_ = hssfSheet.getFirstRowNum();
    }


    /**
     * コンバータを設定する。
     * @param converter コンバータ
     */
    void setHSSFCellValueConverter(HSSFCellValueConverter converter) {
        Validator.notNull(converter);
        converter_ = converter;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String[] next() {

        if (finished()) return null;
        moved_ = true;
        currentRowNumber_ = getNextRowNumber();
        return current();
    }


    private String[] doGet(int rowNumber) {
        try {
            HSSFRow row = hssfSheet_.getRow(rowNumber);
            return convertToStringArray(row);
        } catch (RuntimeException e) {
            throw new UnexpectedProblemException(e,
                    "Read error. sheet=[", sheetName_, "] row=[", currentRowNumber_, "]");
        }
    }

    @Override
    public String[] current() {
        if (finished()) return null;
        if (moved_) {
            moved_ = false;
            String[] current = doGet(currentRowNumber_);
            currentRow_ = isBlankRow(current) ? new String[0] : current;
        }
        return CollectionUtils.copy(currentRow_);
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasNext() {
        return !finished() && getNextRowNumber() != END_OF_CONTEXT;
    }

    private boolean finished() {
        return currentRowNumber_ == END_OF_CONTEXT;
    }

    /**
     * 次の行番号を取得する。
     * 有効でない行はスキップされる。
     * 有効でない行とは、コメント行、非表示行を指す。
     *
     * @return 次の行番号
     */
    @Override
    protected final int getNextRowNumber() {
        try {
            return doGetNextRowNumber();
        } catch (RuntimeException e) {
            throw new UnexpectedProblemException(e,
                    "Read error. sheet=[", sheetName_, "] row=[", currentRowNumber_, "]");
        }
    }

    private int doGetNextRowNumber() {
        if (finished()) return END_OF_CONTEXT;

        for (int searchIdx = currentRowNumber_ + 1; searchIdx <= hssfSheet_
                .getLastRowNum(); searchIdx++) {
            if (isValidRow(hssfSheet_.getRow(searchIdx))) {
                return searchIdx;
            }
        }
        return END_OF_CONTEXT;
    }

    /**
     * 列をString配列に変換する
     *
     * @param row HSSFRow
     * @return 変換結果
     */
    private String[] convertToStringArray(HSSFRow row) {
        if (row == null) return new String[0];
        List<String> cells = new ArrayList<String>();
        // cellIteratorだとうまくいかないぞ。。
        //
        short last = getEffectiveCellCount(row);
        for (int cellIndex = 0; cellIndex < last; cellIndex++) {
            HSSFCell cell = row.getCell(cellIndex);
            String cellValue = convertToString(cell);
            // コメントセルに到達した場合は読み込み終了
            if (isCommentCell(cellValue)) break;
            cells.add(cellValue);
        }
        return cells.toArray(new String[cells.size()]);
    }

    /**
     * POIの不具合っぽいの対処用メソッド。<br/>
     * HSSFRow#getPhysicalNumberOfCells()が正しい値を返却しない問題に対処<br/>
     * POIのバージョンがあがったらいらんかも
     *
     * @param row 対象行
     * @return 有効セル数
     */
    private short getEffectiveCellCount(HSSFRow row) {
        short effectiveCellCount = 0;    // 有効セル数
        short nullCellCount = 0;    // nullセル数

        for (int idx = 0; idx < row.getLastCellNum(); idx++) {
            if (row.getCell(idx) == null) {
                nullCellCount++;
            } else {
                effectiveCellCount++;
                if (nullCellCount != 0) {
                    // nullのセルを発見後に有効セルを発見した場合、
                    // それまでのnullのセル数を追加する。
                    effectiveCellCount += nullCellCount;
                    nullCellCount = 0;
                }
            }
        }
        return effectiveCellCount;
    }

    /**
     * セルを文字列に変換する。
     *
     * @param cell HSFFセル
     * @return 文字列表現
     */
    private String convertToString(HSSFCell cell) {
        return converter_.convert(cell);
    }

    /**
     * 有効な行であるか判定する。
     * <ul>
     * <li>コメント行、または非表示行の場合は、無効な行と判定される。</li>
     * <li>nullまたは要素数0の場合は、有効行と判定される</li>
     * </ul>
     *
     * @param row 判定対象となる行
     * @return 判定結果
     */
    private boolean isValidRow(HSSFRow row) {
        if (row == null || row.getLastCellNum() == 0) {
            return true;
        }
        return !isHidden(row) && !isCommentRow(row);
    }

    /**
     * 非表示行であるか判定する。
     *
     * @param row 判定対象となる行
     * @return 判定結果
     */
    private boolean isHidden(HSSFRow row) {
        // http://java-house.jp/ml/archive/j-h-b/052531.html
        return row.getZeroHeight();
    }


    /**
     * コメント行であるか判定する
     *
     * @param row 判定対象となる行
     * @return 判定結果
     */
    private boolean isCommentRow(HSSFRow row) {
        HSSFCell firstCell = row.getCell(0);
        return isCommentCell(convertToString(firstCell));
    }

    /** {@inheritDoc} */
    @Override
    public String getSheetName() {
        return sheetName_;
    }


}
